package org.xx.armory.spring5.junit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.xx.armory.commons.ForLogging;

import javax.xml.stream.XMLStreamException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static java.sql.Types.BIGINT;
import static java.sql.Types.BINARY;
import static java.sql.Types.BIT;
import static java.sql.Types.BLOB;
import static java.sql.Types.BOOLEAN;
import static java.sql.Types.CHAR;
import static java.sql.Types.CLOB;
import static java.sql.Types.DATE;
import static java.sql.Types.DECIMAL;
import static java.sql.Types.DOUBLE;
import static java.sql.Types.FLOAT;
import static java.sql.Types.INTEGER;
import static java.sql.Types.LONGNVARCHAR;
import static java.sql.Types.LONGVARBINARY;
import static java.sql.Types.LONGVARCHAR;
import static java.sql.Types.NCHAR;
import static java.sql.Types.NUMERIC;
import static java.sql.Types.NVARCHAR;
import static java.sql.Types.REAL;
import static java.sql.Types.SMALLINT;
import static java.sql.Types.TIME;
import static java.sql.Types.TIMESTAMP;
import static java.sql.Types.TINYINT;
import static java.sql.Types.VARBINARY;
import static java.sql.Types.VARCHAR;
import static java.util.Collections.reverse;
import static org.xx.armory.commons.StringConverter.parseBigDecimal;
import static org.xx.armory.commons.StringConverter.parseBoolean;
import static org.xx.armory.commons.StringConverter.parseByte;
import static org.xx.armory.commons.StringConverter.parseBytes;
import static org.xx.armory.commons.StringConverter.parseDouble;
import static org.xx.armory.commons.StringConverter.parseFloat;
import static org.xx.armory.commons.StringConverter.parseInteger;
import static org.xx.armory.commons.StringConverter.parseShort;
import static org.xx.armory.commons.StringConverter.parseTime;
import static org.xx.armory.commons.StringConverter.parseTimestampWithLeniency;
import static org.xx.armory.commons.Validators.rejectIfBlank;
import static org.xx.armory.commons.Validators.rejectIfEmpty;
import static org.xx.armory.commons.Validators.rejectIfNull;
import static org.xx.armory.spring5.jdbc.SqlTypeUtils.sqlTypeName;
import static org.xx.armory.spring5.junit.DbUnitTableReader.readAllRows;

/**
 * 用于对数据库进行单元测试的工具类。
 */
public abstract class DbUnitHelper {
    @ForLogging
    protected final Logger logger = LoggerFactory.getLogger(DbUnitHelper.class);
    private final Map<String, List<Column>> tables;
    private final Map<String, List<Row>> tableRows;
    @Autowired
    protected JdbcTemplate jdbcTemplate;

    public DbUnitHelper() {
        this.tables = new LinkedHashMap<>();
        this.tableRows = new LinkedHashMap<>();
    }

    /**
     * 执行插入数据或者更新数据的SQL。
     *
     * @param sql
     *         待执行的SQL。
     * @param args
     *         参数。
     * @return 插入或者更新的记录数。
     * @throws DataAccessException
     *         如果执行SQL出错。
     */
    public int update(
            String sql,
            Object... args
    )
            throws DataAccessException {
        return this.jdbcTemplate.update(sql, args);
    }

    /**
     * 执行查询列表数据的SQL。
     *
     * @param sql
     *         待执行的SQL。
     * @param args
     *         参数。
     * @return 查询结果。
     * @throws DataAccessException
     *         如果执行SQL出错。
     */
    public List<Map<String, Object>> list(
            String sql,
            Object... args
    )
            throws DataAccessException {
        return this.jdbcTemplate.queryForList(sql, args);
    }

    /**
     * 执行查询单行数据的SQL。
     *
     * @param sql
     *         待执行的SQL。
     * @param args
     *         参数。
     * @return 查询结果。
     * @throws DataAccessException
     *         如果执行SQL出错。
     */
    public Map<String, Object> singleRow(
            String sql,
            Object... args
    )
            throws DataAccessException {
        return this.jdbcTemplate.queryForMap(sql, args);
    }

    /**
     * 执行查询单个数据的SQL。
     *
     * @param sql
     *         待执行的SQL。
     * @param args
     *         参数。
     * @return 查询结果。
     * @throws DataAccessException
     *         如果执行SQL出错。
     */
    public Object scalar(
            String sql,
            Object... args
    )
            throws DataAccessException {
        return this.jdbcTemplate.queryForObject(sql, Object.class, args);
    }

    /**
     * 从XML文件中加载数据
     *
     * @param tableName
     *         数据库表名
     * @param resourceClassPath
     *         包含XML文件的资源的类路径。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符，或者参数{@code stream}是{@code null}。
     * @throws IOException
     *         如果读取资源时出现错误。
     * @throws XMLStreamException
     *         如果加载数据时出现错误。
     * @see ClassPathResource
     */
    public void readXml(
            String tableName,
            String resourceClassPath
    )
            throws XMLStreamException, IOException {
        readXml(tableName, new ClassPathResource(resourceClassPath));
    }

    /**
     * 从XML文件中加载数据
     *
     * @param tableName
     *         数据库表名
     * @param resource
     *         包含XML文件的资源。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符，或者参数{@code stream}是{@code null}。
     * @throws IOException
     *         如果读取资源时出现错误。
     * @throws XMLStreamException
     *         如果加载数据时出现错误。
     */
    public void readXml(
            String tableName,
            Resource resource
    )
            throws XMLStreamException, IOException {
        try (final var stream = resource.getInputStream()) {
            readXml(tableName, stream);
        }
    }

    /**
     * 从XML文件中加载数据
     *
     * @param tableName
     *         数据库表名
     * @param stream
     *         包含XML文件的流
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符，或者参数{@code stream}是{@code null}。
     */
    public void readXml(
            String tableName,
            InputStream stream
    ) {
        tableName = rejectIfBlank(tableName, "tableName").trim();
        rejectIfNull(stream, "stream");

        final var columns = this.tables.get(tableName);
        if (columns == null) {
            throw new IllegalArgumentException("Illegal table name: " + tableName);
        }

        final var rows = readAllRows(stream)
                .stream()
                .map(row -> {
                    final var values = new ArrayList<>(columns.size());

                    for (final var column : columns) {
                        final var s = row.get(column.getName());
                        if (s != null) {
                            values.add(parseValue(column.getName(), column.getType(), s));
                        } else {
                            values.add(null);
                        }
                    }

                    return new Row(values);
                }).collect(Collectors.toList());

        logger.debug("Read {} row(s) of table {}", rows.size(), tableName);

        this.tableRows.put(tableName, rows);
    }

    /**
     * 清除已加载的元数据和行。
     *
     * <strong>应当在每个TestSuite之前调用。</strong>
     */
    public void resetData() {
        this.tables.clear();
        this.tableRows.clear();
    }

    /**
     * 从数据库中加载表的元数据
     *
     * @param tableName
     *         数据库表名
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符，或者参数{@code stream}是{@code null}。
     * @throws DataAccessException
     *         如果执行SQL出错。
     */
    public void loadMetaData(
            String tableName
    )
            throws DataAccessException {
        this.tables.put(tableName, metaData(tableName));
    }

    /**
     * 将所有从XML文件中加载的记录写入到数据库表。
     * <p><strong>按照加载元数据的次序插入记录，按照相反的次序删除原有的记录。</strong></p>
     *
     * @param clean
     *         写入时是否删除原有的所有记录。
     * @throws DataAccessException
     *         如果执行SQL出错。
     * @see #loadMetaData(String)
     * @see #clearAll()
     */
    public void insertAll(
            boolean clean
    )
            throws DataAccessException {
        if (clean) {
            clearAll();
        }

        for (final var tableName : this.tables.keySet()) {
            final var columns = this.tables.get(tableName);
            final var rows = this.tableRows.get(tableName);

            insert(tableName, columns, rows);
        }
    }

    /**
     * 删除所有被加载了元数据的数据库表中的所有记录。
     *
     * <p>
     * <strong>按照加载元数据的<u>相反次序</u>删除记录。</strong>
     * <strong>只有通过{@link #loadMetaData(String)}加载了元数据的表的记录才会被删除！</strong>
     * </p>
     *
     * @throws DataAccessException
     *         如果执行SQL出错。
     * @see #loadMetaData(String)
     */
    public void clearAll()
            throws DataAccessException {
        var tableNames = new ArrayList<>(tables.keySet());
        reverse(tableNames);
        for (final String tableName : tableNames) {
            clear(tableName);
        }
    }

    /**
     * 重置序列。
     *
     * @param name
     *         序列的名字。
     */
    public void resetSequence(
            String name
    ) {
        resetSequence(name, 0);
    }

    /**
     * 重置序列。
     *
     * @param name
     *         序列的名字。
     * @param value
     *         重置的目标值。
     */
    public void resetSequence(
            String name,
            int value
    ) {
        resetSequenceValue(name, value);
        this.logger.debug("Reset sequence {} to {}", name, value);
    }

    /**
     * 重置序列。
     *
     * @param name
     *         序列的名字。
     * @param value
     *         重置的目标值。
     */
    protected void resetSequenceValue(
            String name,
            int value
    ) {
        throw new AssertionError();
    }


    /**
     * 根据列类型将字符串解析为对应的值。
     *
     * @param name
     *         列名称。
     * @param sqlType
     *         列类型。
     * @param s
     *         原始字符串。
     * @return 解析后的值。
     * @throws IllegalArgumentException
     *         如果参数{@code s}是{@code null}，或者参数{@code s}无法被解析为期望的类型，或者不支持参数 {@code sqlType}指定的列类型。
     */
    private Object parseValue(
            String name,
            int sqlType,
            String s
    ) {
        rejectIfNull(s, "s");

        // 对于字符串类型，不需要转化，直接返回。
        if (sqlType == CHAR || sqlType == VARCHAR || sqlType == NCHAR || sqlType == NVARCHAR || sqlType == LONGVARCHAR || sqlType == LONGNVARCHAR || sqlType == CLOB) {
            return s;
        }

        final Object v;
        switch (sqlType) {
            case BIGINT:
            case DECIMAL:
            case NUMERIC:
                v = parseBigDecimal(s, null);
                break;
            case BINARY:
            case BLOB:
            case VARBINARY:
            case LONGVARBINARY:
                v = parseBytes(s, null);
                break;
            case BIT:
            case BOOLEAN:
                v = parseBoolean(s, null);
                break;
            case DATE:
            case TIMESTAMP:
                v = parseTimestampWithLeniency(s, null);
                break;
            case REAL:
            case DOUBLE:
                v = parseDouble(s, null);
                break;
            case FLOAT:
                v = parseFloat(s, null);
                break;
            case INTEGER:
                v = parseInteger(s, null);
                break;
            case SMALLINT:
                v = parseShort(s, null);
                break;
            case TIME:
                v = parseTime(s, null);
                break;
            case TINYINT:
                v = parseByte(s, null);
                break;
            default:
                throw new IllegalArgumentException("Unsupported sql type " + sqlTypeName(sqlType) + " of column [" + name + "]");
        }

        if (v == null) {
            throw new IllegalArgumentException("Cannot parse \"" + s + "\" as " + sqlTypeName(sqlType) + " for column [" + name + "]");
        }

        return v;
    }

    /**
     * 向指定的表中插入记录。
     *
     * @param tableName
     *         表名。
     * @param columns
     *         所有待插入的列的元数据。
     * @param rows
     *         所有待插入的行的数据。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符，或者参数{@code columnNames}是{@code null}或者长度为0，
     *         或者参数{@code columnNames}的某个列名是{@code null}或者只包含空白字符。
     * @throws DataAccessException
     *         如果执行SQL出错。
     */
    private void insert(
            String tableName,
            List<? extends Column> columns,
            List<? extends Row> rows
    )
            throws DataAccessException {
        tableName = rejectIfBlank(tableName, "tableName").trim();
        rejectIfEmpty(columns, "columns");

        final var sql = insertSql(tableName, columns.stream().map(Column::getName).toArray(String[]::new));
        logger.trace("INSERT SQL: {}", sql);

        if (rows == null || rows.isEmpty()) {
            logger.info("No rows found of table {}", tableName);
            return;
        }

        for (final var row : rows) {
            if (row.size() < columns.size()) {
                throw new IllegalStateException("Expected " + columns.size() + " values, but " + row.size() + " got");
            }
        }

        final var allTypes = columns.stream().mapToInt(Column::getType).toArray();
        var c = 0;
        for (final var row : rows) {
            c += this.jdbcTemplate.update(sql, row.allValues(), allTypes);
        }

        logger.debug("Inserted {} row(s) into table {}", c, tableName);
    }

    /**
     * 清除指定表的所有数据。
     *
     * @param tableName
     *         表名。
     * @throws DataAccessException
     *         如果执行SQL出错。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符。
     */
    private void clear(
            String tableName
    )
            throws DataAccessException {
        tableName = rejectIfBlank(tableName, "tableName").trim();

        final var sql = cleanSql(tableName);
        logger.trace("CLEAR TABLE SQL: {}", sql);

        final var c = this.jdbcTemplate.update(sql);

        logger.debug("Deleted {} row(s) from table {}", c, tableName);
    }

    /**
     * 获取指定表的所有列的元数据。
     *
     * @param tableName
     *         表名。
     * @return 指定表的所有列的元数据。
     * @throws DataAccessException
     *         如果执行SQL出错。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符。
     */
    private List<Column> metaData(
            String tableName
    )
            throws DataAccessException {
        tableName = rejectIfBlank(tableName, "tableName").trim().toUpperCase();

        final var sql = "SELECT * FROM " + escapeIdentifier(tableName) + " WHERE 0 = 1";
        logger.trace("INSPECT TABLE META DATA SQL: {}", sql);

        final var rowSet = this.jdbcTemplate.queryForRowSet(sql);
        final var rowSetMetaData = rowSet.getMetaData();
        final var c = rowSetMetaData.getColumnCount();
        final var result = new ArrayList<Column>(c);

        for (var i = 0; i < c; ++i) {
            result.add(new Column(rowSetMetaData.getColumnName(i + 1), rowSetMetaData.getColumnType(i + 1)));
        }

        logger.debug("{} column(s) found of table {}", c, tableName);

        return result;
    }


    /**
     * 构造执行INSERT记录的SQL命令。
     * <p>返回的SQL命令形如： {@literal INSERT INTO <table name> (<column name1>, <column name2>) VALUES (?, ?, ...) }</p>
     *
     * @param tableName
     *         表名。
     * @param columnNames
     *         列名数组。
     * @return 用于向指定的表INSERT记录的SQL命令。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符，或者参数{@code columnNames}是{@code null}或者长度为0，
     *         或者参数{@code columnNames}的某个列名是{@code null}或者只包含空白字符。
     */
    private String insertSql(
            String tableName,
            String[] columnNames
    ) {
        tableName = rejectIfBlank(tableName, "tableName").trim().toUpperCase();
        rejectIfEmpty(columnNames, "columnNames");

        final var sb = new StringBuilder();
        sb.append("INSERT INTO ").append(escapeIdentifier(tableName)).append(" (");

        for (var i = 0; i < columnNames.length; ++i) {
            sb.append(escapeIdentifier(columnNames[i]));
            if (i < columnNames.length - 1) {
                sb.append(",");
            }
        }
        sb.append(") VALUES (");

        for (var i = 0; i < columnNames.length; ++i) {
            sb.append("?");
            if (i < columnNames.length - 1) {
                sb.append(",");
            }
        }
        sb.append(")");

        return sb.toString();
    }

    /**
     * 转义标识符，如果标识符和关键字重名，那么需要包装之后才能用于SQL语句中。
     *
     * @param identifier
     *         待转义的标识符。
     * @return 转义后的标识符。
     */
    protected String escapeIdentifier(
            String identifier
    ) {
        return identifier;
    }

    /**
     * 创建执行清除表所有数据的SQL命令。
     *
     * @param tableName
     *         表名。
     * @return 用于清除指定表的所有记录的SQL命令。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符。
     */
    private String cleanSql(
            String tableName
    ) {
        tableName = rejectIfBlank(tableName, "tableName").trim().toUpperCase();
        return "DELETE FROM " + escapeIdentifier(tableName);
    }

    /**
     * 数据库表的列
     *
     * @author Haart
     */
    static final class Column {
        private final String name;
        private final int type;

        /**
         * 构造数据库表的列
         *
         * @param name
         *         列名
         * @param type
         *         列类型
         */
        public Column(
                String name,
                int type
        ) {
            this.name = name;
            this.type = type;
        }

        /**
         * 获取列名
         *
         * @return the name
         */
        public String getName() {
            return this.name;
        }

        /**
         * 获取列类型
         *
         * @return the type
         */
        public int getType() {
            return this.type;
        }
    }

    /**
     * 数据库表的一条记录
     *
     * @author Haart
     */
    static final class Row {
        private final List<Object> values;

        /**
         * 构造数据库表的一条记录
         *
         * @param values
         *         该记录包含的所有数据
         */
        public Row(
                Collection<?> values
        ) {
            rejectIfEmpty(values, "values");

            this.values = new ArrayList<>(values);
        }

        /**
         * 获取指定列的值(从0开始)
         *
         * @param index
         *         列下标
         * @return 指定列的值
         * @throws ArrayIndexOutOfBoundsException
         *         如果下标越界。
         */
        public final Object get(
                int index
        ) {
            return this.values.get(index);
        }

        /**
         * 获取该行所有列的数据。
         *
         * @return 所有列的数据。
         */
        public final Object[] allValues() {
            return this.values.toArray(new Object[0]);
        }

        /**
         * 获取每条记录包含的数据的个数。
         *
         * @return 每条记录包含的数据的个数。
         */
        public final int size() {
            return this.values.size();
        }
    }
}
